import { terminal as tk } from 'terminal-kit';
import { resolve } from 'path';
import { readdirSync, rmSync } from 'fs';
import { extractErrorMessage, getNpxCacheDir, isDir, readJson } from './lib';

const { stdout } = process;

type CallbackParams = {
  dir: string,
}

type Callback = (p: CallbackParams) => void | Promise<void>;

type NpxModulePackage = {
  dependencies: Record<string, string>
}

type NpxModule = {
  version: string,
  path: string,
}

type NpxModules = Record<string, NpxModule[]>

export function prepare(): CallbackParams {
  const dir = getNpxCacheDir();
  if (!isDir(dir)) {
    throw new Error('There is no npx cache directory on this machine.');
  }
  return { dir };
}

export async function execute(callback: Callback) {
  try {
    await callback(prepare());
  } catch (e) {
    tk.red(extractErrorMessage(e) || 'Unknown error!');
  } finally {
    tk('\n');
  }
}

export function outputNpxCacheDir() {
  execute(({ dir }) => {
    tk('The npm cache directory:\n');
    tk.blue(dir);
    tk('\n');
    console.log(`${dir}:0`);
    tk.gray('(Click the link will open it in a new explorer!)');
  });
}

function canIgnoreModule(name: string, keywords?: string[], ignore?: string[], names?: string[]): boolean {
  if (!name) return true;
  if (ignore != null && ignore.length > 0 && ignore.includes(name)) return true;
  if (keywords != null && keywords.length > 0) {
    return keywords.filter(it => name.indexOf(it) > -1).length <= 0;
  }
  if (names != null && names.length > 0) {
    return !names.includes(name);
  }
  return false;
}

function getNpxCacheModules(dir: string, keywords?: string[], ignore?: string[], names?: string[]): NpxModules {
  const modules: NpxModules = {};
  readdirSync(dir).forEach(it => {
    const path = resolve(dir, it);
    const pkg = readJson<NpxModulePackage>(resolve(path, 'package.json'));
    if (pkg == null || pkg.dependencies == null) return;
    const deps = Object.keys(pkg.dependencies);
    const name = deps[0];
    if (canIgnoreModule(name, keywords, ignore, names)) return;
    if (modules[name] == null) {
      modules[name] = [];
    }
    modules[name].push({
      version: pkg.dependencies[name],
      path,
    });
  });
  return Object.keys(modules).sort().reduce((record, key) => {
    record[key] = modules[key];
    return record;
  }, {} as NpxModules);
}

export function listNpxCacheModules(keywords?: string[]) {
  execute(({ dir }) => {
    const modules = getNpxCacheModules(dir, keywords);
    Object.entries(modules).forEach(([name, items]) => {
      tk('- ');
      tk.green(name);
      tk('\n');
      items.forEach(({ version, path }) => {
        console.log('  ' + version);
        stdout.write('  ');
        stdout.write(`${path}:0`);
        tk('\n');
      });
    });
  });
}

function confirmRemove(
  action: 'Clear' | 'Remove',
  modules: NpxModules,
  onRemove: (modules: NpxModules) => void,
) {
  const count = Object.keys(modules).length;
  if (count <= 0) {
    throw new Error('There is no npx cache module!');
  }
  if (count === 1) {
    tk('There is ');
    tk.yellow.bold(count);
    tk(' npx cache module. ');
    tk.red(`${action} it? `);
  } else {
    tk('There are ');
    tk.yellow.bold(count);
    tk(' npx cache modules. ');
    tk.red(`${action} them? `);
  }
  tk('[y/n]');
  tk.yesOrNo(
    { yes: ['y', 'ENTER'], no: ['n'] },
    async (err, res) => {
      if (res) {
        tk.green('Yes!\n');
        await onRemove(modules);
      } else {
        tk.red('No!');
      }
      process.exit();
    },
  );
}

function removeModule(name: string, { version, path }: NpxModule) {
  tk.red('rm ');
  tk.green(name);
  stdout.write(` ${version}: `);
  tk.blue(path);
  tk('\n');
  rmSync(path, { recursive: true });
}

// @see https://github.com/return-0x0/node-clear-npx-cache/blob/master/src/index.ts
export function clearNpxCacheModules() {
  execute(({ dir }) => {
    confirmRemove(
      'Clear',
      getNpxCacheModules(dir, undefined, ['npx-cache']),
      (modules) => {
        Object.entries(modules).forEach(([name, items]) => {
          items.forEach((it) => removeModule(name, it));
        });
      },
    );
  });
}

export function removeNpxCacheModules(names?: string[]) {
  execute(({ dir }) => {
    if (names == null || names.length <= 0) {
      throw new Error('Unspecified modules name');
    } else {
      tk(`Remove module${names.length > 1 ? 's' : ''}: `);
      names.forEach((name, idx) => {
        if (idx > 0) tk.gray(', ');
        tk.green(name);
      });
      tk('\n');
    }
    confirmRemove(
      'Remove',
      getNpxCacheModules(dir, undefined, ['npx-cache'], names),
      (modules) => {
        Object.entries(modules).forEach(([name, items]) => {
          items.forEach((it) => removeModule(name, it));
        });
      },
    );
  });
}
